/*
 * Copyright (C) 2008 Peter Kling
 *
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 *
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 *
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * 'A Kind of Billiard'. If not, see <http://www.gnu.org/licenses/>.
 */

package cg2.misc;

import javax.media.opengl.GL;

/**
 * @brief Allows easy rendering to a texture.
 */
public class TextureRenderer {
	private int[] targetFramebuffer = { 0 };
	private int[] targetDSBuffer    = { 0 };
	private int[] targetTexture     = { 0 };
	
	private int buffWidth  = 0;
	private int buffHeight = 0;

	/**
	 * @brief Constructs a TextureRenderer.
	 */
	public TextureRenderer(GL gl) {
	}
	
	/**
	 * @brief Binds the texture of this render target to the current texture unit.
	 */
	public void bindTexutre(GL gl) {
		gl.glBindTexture(GL.GL_TEXTURE_2D, targetTexture[0]);
	}
	
	/**
	 * @brief Checks for errors related to frame buffer objects.
	 */
	public static void checkFramebufferStatus(GL gl) {
		int status;
		status = gl.glCheckFramebufferStatusEXT(GL.GL_FRAMEBUFFER_EXT);
		switch (status) {
		case GL.GL_FRAMEBUFFER_COMPLETE_EXT:
			return;

		case GL.GL_FRAMEBUFFER_UNSUPPORTED_EXT:
			System.out.println("Unsupported framebuffer format.");
			break;

		case GL.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
			System.out.println("Framebuffer incomplete, missing attachment.");
			break;

		case GL.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
			System.out.println("Framebuffer incomplete, attached images must have same dimensions.");
			break;

		case GL.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
			System.out.println("Framebuffer incomplete, attached images must have same format.");
			break;

		case GL.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
			System.out.println("Framebuffer incomplete, missing draw buffer.");
			break;

		case GL.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
			System.out.println("Framebuffer incomplete, missing read buffer.");
			break;

		default:
			System.out.println("Error " + status);
			break;
		}
		
		// frame buffer not complete
		System.exit(1);
	}
	
	/**
	 * @brief Disables the frame buffer object by binding the standard frame buffer.
	 */
	public void disable(GL gl) {
		gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);
		gl.glPopAttrib();
	}
	
	/**
	 * @brief Enables the frame buffer object by binding it.
	 */
	public void enable(GL gl) {
		gl.glPushAttrib(GL.GL_VIEWPORT_BIT);
		gl.glBindTexture(GL.GL_TEXTURE_2D, 0);
		gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, targetFramebuffer[0]);
		gl.glViewport(0, 0, buffWidth, buffHeight);
	}

	/**
	 * @brief Initializes the frame buffer object and texture associated with this object.
	 */
	public void init(GL gl, int width, int height) {
		buffWidth  = width;
		buffHeight = height;
		
		// generate and bind frame buffer
		gl.glGenFramebuffersEXT(1, targetFramebuffer, 0);
		gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, targetFramebuffer[0]);
		
		// generate and bind texture
		gl.glGenTextures(1, targetTexture, 0);
		gl.glBindTexture(GL.GL_TEXTURE_2D, targetTexture[0]);
		setTextureParameters(gl);
		gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, width, height, 0, GL.GL_RGBA, GL.GL_FLOAT, null);
		gl.glFramebufferTexture2DEXT(GL.GL_FRAMEBUFFER_EXT, GL.GL_COLOR_ATTACHMENT0_EXT, GL.GL_TEXTURE_2D,
				targetTexture[0], 0);
		checkFramebufferStatus(gl);
		
		// generate and bind depth/stencil buffer
		gl.glGenRenderbuffersEXT(1, targetDSBuffer, 0);
		gl.glBindRenderbufferEXT(GL.GL_RENDERBUFFER_EXT, targetDSBuffer[0]);
		gl.glRenderbufferStorageEXT(GL.GL_RENDERBUFFER_EXT, GL.GL_DEPTH24_STENCIL8_EXT, width, height);
		gl.glFramebufferRenderbufferEXT(GL.GL_FRAMEBUFFER_EXT, GL.GL_DEPTH_ATTACHMENT_EXT, GL.GL_RENDERBUFFER_EXT,
				targetDSBuffer[0]);
		gl.glFramebufferRenderbufferEXT(GL.GL_FRAMEBUFFER_EXT, GL.GL_STENCIL_ATTACHMENT_EXT, GL.GL_RENDERBUFFER_EXT,
				targetDSBuffer[0]);
		checkFramebufferStatus(gl);
		
		// disable frame buffer object
		gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);
	}
	
	/**
	 * @brief Renders the texture associated with this object onto the whole screen.  
	 */
	public void renderOnscreen(GL gl) {
		// configure matrices
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glPushMatrix();
		gl.glLoadIdentity();
		gl.glOrtho(-1., 1., -1., 1., -1., 1.);
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glPushMatrix();
		gl.glLoadIdentity();
		
		// draw texture onto the whole screen
		gl.glBegin(GL.GL_QUADS);
		gl.glTexCoord2d(0., 0.); gl.glVertex2d(-1., -1.);
		gl.glTexCoord2d(1., 0.); gl.glVertex2d( 1., -1.);
		gl.glTexCoord2d(1., 1.); gl.glVertex2d( 1.,  1.);
		gl.glTexCoord2d(0., 1.); gl.glVertex2d(-1.,  1.);
		gl.glEnd();
		
		// restore projection matrix and reset matrix mode
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glPopMatrix();
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glPopMatrix();
	}
	
	/**
	 * @brief Resizes the texture associated with this object.
	 */
	public void resize(GL gl, int width, int height) {
		buffWidth  = width;
		buffHeight = height;
		
		// resize texture
		gl.glBindTexture(GL.GL_TEXTURE_2D, targetTexture[0]);
		gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, width, height, 0, GL.GL_RGBA, GL.GL_FLOAT, null);
		
		// resize depth/stencil buffer
		gl.glBindRenderbufferEXT(GL.GL_RENDERBUFFER_EXT, targetDSBuffer[0]);
		gl.glRenderbufferStorageEXT(GL.GL_RENDERBUFFER_EXT, GL.GL_DEPTH24_STENCIL8_EXT, width, height);
	}

	/**
	 * @brief Sets texture parameters used for textures as render targets.
	 */
	public static void setTextureParameters(GL gl) {
		gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
		gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
		gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S,     GL.GL_CLAMP_TO_EDGE);
		gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T,     GL.GL_CLAMP_TO_EDGE);
	}
}
